package com.nageoffer.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.shortlink.project.common.convention.exception.ClientException;
import com.nageoffer.shortlink.project.common.convention.exception.ServiceException;
import com.nageoffer.shortlink.project.common.enums.ValidDateTypeEnum;
import com.nageoffer.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.nageoffer.shortlink.project.dao.entity.*;
import com.nageoffer.shortlink.project.dao.mapper.*;
import com.nageoffer.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.nageoffer.shortlink.project.dto.req.*;
import com.nageoffer.shortlink.project.dto.resp.*;
import com.nageoffer.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.nageoffer.shortlink.project.service.LinkStatsTodayService;
import com.nageoffer.shortlink.project.service.ShortLinkService;
import com.nageoffer.shortlink.project.toolkit.HashUtil;
import com.nageoffer.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.*;

/**
 * 短链接接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;       // 布隆过滤器

    private final ShortLinkGotoMapper shortLinkGotoMapper;

    private final StringRedisTemplate stringRedisTemplate;      // 一般用于缓存 键值对 都是 String 类型对象

    private final RedissonClient redissonClient;        // Redisson 实现的分布式锁

    private final LinkAccessStatsMapper linkAccessStatsMapper;      // 短链接基础访问监控持久层

    private final LinkLocaleStatsMapper linkLocaleStatsMapper;      // 地区统计访问持久层

    private final LinkOsStatsMapper linkOsStatsMapper;      // 短链接操作系统访问统计持久层

    private final LinkBrowserStatsMapper linkBrowserStatsMapper;        // 短链接浏览器访问统计持久层

    private final LinkAccessLogsMapper linkAccessLogsMapper;        // 访问日志监控持久层

    private final LinkDeviceStatsMapper linkDeviceStatsMapper;      // 访问设备监控持久层

    private final LinkNetworkStatsMapper linkNetworkStatsMapper;        // 访问网络监控持久层

    private final LinkStatsTodayMapper linkStatsTodayMapper;        // 短链接今日统计持久层

    private final LinkStatsTodayService linkStatsTodayService;      // 短链接今日统计接口层

    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;

    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;        // 跳转域名白名单配置文件

    @Value("${short-link.stats.locale.amap-key}")       // 从外部配置源（如属性文件、环境变量等）将值动态注入到Spring管理的Bean中，实现配置与代码的解耦
    private String statsLocalAmapKey;

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @Transactional(rollbackFor = Exception.class)       // 保证多次数据库插入操作的同时成功或失败
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        // 验证跳转链接白名单
        verificationWhitelist(requestParam.getOriginUrl());

        String shortLinkSuffix = generateSuffix(requestParam);      // 根据原始连接 生成 短链接 做 后缀
        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortLinkSuffix; // 完整短链接

        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                // 以上为 ShortLinkCreateReqDTO 类中的属性
                // 以下为 额外新增属性
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .fullShortUrl(fullShortUrl)     // 短链接：域名/Base62编码
                .shortUri(shortLinkSuffix)
                .favicon(getFavicon(requestParam.getOriginUrl()))       // 获取原始链接图标
                .build();

        // 创建 ShortLinkGotoDO 对象，用来做短链接路由功能
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();

        try {
            // 这里涉及到两次插入数据库的操作，需要加上事务来控制两次插入操作的同时成功或失败
            baseMapper.insert(shortLinkDO);     // 布隆过滤器 存在一定的误判，此时插入数据库的时候就会报 索引的key重复 错误
            // 将当前新增的 完整短链接 及 对应的gid 插入到路由表中，后期可以通过 完整短链接 路由到对应的 gid
            shortLinkGotoMapper.insert(shortLinkGotoDO);
        } catch (DuplicateKeyException ex) {        // 生成的短链接在数据库中已存在才会进入当前 catch 分支
            throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
        }

        // 将新增到数据库中的短链接提前加载进缓存，做缓存预热，就不用等用户第一次访问时才将数据构建到缓存中，减少响应时间
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),       // 这个键部分记得加 goto 的标识
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidDate(requestParam.getValidDate()), TimeUnit.MILLISECONDS      // 设置短链接在缓存中的过期时间
        );

        // 短链接确实在布隆过滤器和数据库中都不存在，那么 该短链接插入数据库成功，同时将该 完整短链接 添加到 布隆过滤器
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);

        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())     // 拼接上协议：http://
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            // 将 ShortLinkBatchCreateReqDTO 转换成 ShortLinkCreateReqDTO这个创建单个短链接时的请求参数对象
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                // 调用 createShortLink 方法依次创建短链接
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                // 构建 ShortLinkBaseInfoRespDTO 对象，用于返回给前端
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    /**
     * 对输入 字符串原始连接  进行哈希 并 转换为 Base62 编码
     *
     * @param requestParam 创建短链接 请求参数
     * @return 后缀（短链接）
     */
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        // 生成的 短链接（Base62编码）可能会产生冲突，需要重试
        int customGenerationCount = 0;      // 当前生成的短链接次数
        String shortUri;
        while (true) {
            // 设置最大重试次数
            if (customGenerationCount > 10) throw new ServiceException("短链接频繁或重复生成，请稍后再试！");

            String originUrl = requestParam.getOriginUrl();     // 获取原始链接
            // 原先写法：原始链接加上 系统当前时间戳 可以极大地降低 生成的短链接 冲突的概率
            // 存在一个问题：可能有大量用户在同一毫秒内同时去请求创建短链接，这样会增加短链接重复生成的概率
            originUrl += UUID.randomUUID().toString();      // 改成 原始链接加上 随机UUID 来降低短链接重复生成的概率

            shortUri = HashUtil.hashToBase62(originUrl);        // 生成短链接

            // 查询当前短链接在布隆过滤器中是否存在（查询数据库的性能太低了，走缓存才能支持高并发）
            // 当前 生成的短链接在布隆过滤器当中不存在
            // 但是 布隆过滤器 存在一定的误判，此时插入数据库的时候就会报 索引的key重复 错误
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shortUri)) {
                break;      // 如果当前生成的短链接不存在于布隆过滤器中，那它就一定不存在
            }
            // 存在
            customGenerationCount++;  //  生成次数加一
        }
        return shortUri;
    }

    // 声明式事务控制
    @Transactional(rollbackFor = Exception.class)       // 指定触发回滚的异常类型：Exception
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        // 验证跳转短链接白名单
        verificationWhitelist(requestParam.getOriginUrl());

        // 由于我们在 t_link_%d 表中采用 gid 来作为分表的键，如果说切换分组的话，那么根据 新的分组gid 就无法找到我们这条修改过的短链接记录
        // 所以说如果前端传递过来的分组 和 这个短链接原来的分组匹配不上，那就要先删除
        LambdaQueryWrapper<ShortLinkDO> lambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)         // 未删除
                .eq(ShortLinkDO::getEnableStatus, 0);   // 启用

        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(lambdaQueryWrapper);
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接不存在");
        }

        // 如果前端传递过来的gid 和 这个短链接原来的gid一致，没有变更，走更新逻辑
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {

            LambdaUpdateWrapper<ShortLinkDO> lambdaUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)     // 未删除
                    .eq(ShortLinkDO::getEnableStatus, 0)        // 启用
                    // 如果说我们的有效期类型为 PERMANENT（0），那有效期设置为null
                    .set(Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);

            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .favicon(hasShortLinkDO.getFavicon())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .build();

            baseMapper.update(shortLinkDO, lambdaUpdateWrapper);
        } else {
            // 因为我们短链接表是以gid作为分片键进行变更的，如果短链接gid变更之后，更新后的短链接它会不在原来的表里面，而是重新分配到一张新的表中
            // 所以需要先删除原来表中的记录，在新的表里面新增一条记录
            // 如果前端传递过来的gid 和 这个短链接原来的gid 不一致，变更了gid，先删除原来的数据，新增到新的表里面

            // 获取分布式写锁，保证同一时刻只能有一个线程进行gid变更，其他线程无法同时获取读锁或写锁
            // 写锁用于修改共享资源，确保在写入时没有其他线程能够访问
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock();
            // 如果当前短链接一直被访问，可能会导致这里的短链接修改不成功
            rLock.lock();
            try {
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())   // 修改之前的gid
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();

                delShortLinkDO.setDelFlag(1);       // 将之前的表记录记为已删除
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);       // 修改状态为已删除

                // 以新的gid创建对象新增到数据库表中
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())     // 新的gid
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);

                // -----------------------------------------------------------------------------------------------------
                // 批量删除原gid对应的 短链接今日统计表中的数据
                LambdaQueryWrapper<LinkStatsTodayDO> statsTodayQueryWrapper = Wrappers.lambdaQuery(LinkStatsTodayDO.class)
                        .eq(LinkStatsTodayDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkStatsTodayDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkStatsTodayDO::getDelFlag, 0);
                List<LinkStatsTodayDO> linkStatsTodayDOList = linkStatsTodayMapper.selectList(statsTodayQueryWrapper);
                if (CollUtil.isNotEmpty(linkStatsTodayDOList)) {
                    // 根据id批量删除
                    linkStatsTodayMapper.deleteBatchIds(linkStatsTodayDOList.stream()
                            .map(LinkStatsTodayDO::getId)
                            .toList()
                    );
                    // 重新设置修改后的gid批量新增到 短链接今日统计表（按gid分片插入）
                    linkStatsTodayDOList.forEach(each -> each.setGid(requestParam.getGid()));
                    linkStatsTodayService.saveBatch(linkStatsTodayDOList);
                }

                // -----------------------------------------------------------------------------------------------------
                // 批量删除原gid对应的 短链接跳转表数据
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                // 根据id删除
                shortLinkGotoMapper.deleteById(shortLinkGotoDO.getId());
                // 重新设置修改后的gid新增到对应的 短链接跳转表（按gig分片插入）
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);

                // -----------------------------------------------------------------------------------------------------
                // 修改原gid对应的 短链接基础访问实体表中gid的值
                LambdaUpdateWrapper<LinkAccessStatsDO> linkAccessStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessStatsDO::getDelFlag, 0);
                // 修改gid
                LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessStatsMapper.update(linkAccessStatsDO, linkAccessStatsUpdateWrapper);  // 更新到数据库

                // -----------------------------------------------------------------------------------------------------
                // 修改原gid对应的 短链接地区统计访问实体表中gid的值
                LambdaUpdateWrapper<LinkLocaleStatsDO> linkLocaleStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkLocaleStatsDO.class)
                        .eq(LinkLocaleStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkLocaleStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkLocaleStatsDO::getDelFlag, 0);
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkLocaleStatsMapper.update(linkLocaleStatsDO, linkLocaleStatsUpdateWrapper);

                // -----------------------------------------------------------------------------------------------------
                // 修改原gid对应的 短链接操作系统统计访问实体表中gid的值
                LambdaUpdateWrapper<LinkOsStatsDO> linkOsStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkOsStatsDO.class)
                        .eq(LinkOsStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkOsStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkOsStatsDO::getDelFlag, 0);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkOsStatsMapper.update(linkOsStatsDO, linkOsStatsUpdateWrapper);

                // -----------------------------------------------------------------------------------------------------
                // 修改原gid对应的 短链接浏览器统计访问实体表中gid的值
                LambdaUpdateWrapper<LinkBrowserStatsDO> linkBrowserStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkBrowserStatsDO.class)
                        .eq(LinkBrowserStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkBrowserStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkBrowserStatsDO::getDelFlag, 0);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkBrowserStatsMapper.update(linkBrowserStatsDO, linkBrowserStatsUpdateWrapper);

                // -----------------------------------------------------------------------------------------------------
                // 修改原gid对应的 短链接访问设备统计访问实体表中gid的值
                LambdaUpdateWrapper<LinkDeviceStatsDO> linkDeviceStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkDeviceStatsDO.class)
                        .eq(LinkDeviceStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkDeviceStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkDeviceStatsDO::getDelFlag, 0);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkDeviceStatsMapper.update(linkDeviceStatsDO, linkDeviceStatsUpdateWrapper);

                // -----------------------------------------------------------------------------------------------------
                // 修改原gid对应的 短链接访问网络统计监控实体表中gid的值
                LambdaUpdateWrapper<LinkNetworkStatsDO> linkNetworkStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkNetworkStatsDO.class)
                        .eq(LinkNetworkStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkNetworkStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkNetworkStatsDO::getDelFlag, 0);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkNetworkStatsMapper.update(linkNetworkStatsDO, linkNetworkStatsUpdateWrapper);

                // -----------------------------------------------------------------------------------------------------
                // 修改原gid对应的 短链接访问日志监控实体表中gid的值
                LambdaUpdateWrapper<LinkAccessLogsDO> linkAccessLogsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessLogsDO.class)
                        .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessLogsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessLogsDO::getDelFlag, 0);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessLogsMapper.update(linkAccessLogsDO, linkAccessLogsUpdateWrapper);
            } finally {
                rLock.unlock();
            }
        }

        // 如果修改之后的有效期类型和之前不一样 或者 修改之后的有效期时间戳和之前不一样，需要将其跳转的缓存给删掉
        if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType())
                || !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())) {
            // 删除跳转的缓存
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            // 这个 before() 方法：比较当前Date对象表示的时间是否早于参数when指定的时间
            // hasShortLinkDO是修改之前的对象，如果原来的短链接有效期在当前时间之前，说明该短链接已经过期了（放到了跳转为空的缓存对象中）
            if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(new Date())) {
                // requestParam 是修改之后的对象，如果修改之后的有效期类型为PERMANENT（0），即永久有效
                // 或者修改之后的有效期时间戳在当前时间之后，说明该短链接没有过期
                if (Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()) || requestParam.getValidDate().after(new Date())) {
                    // 删除该短链接对应的跳转为空的缓存
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }
            }
        }
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());       // 修改分页返回域名
            return result;
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> gidList) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid, count(*) as shortLinkCount")
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .groupBy("gid");

        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);

        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        String serverName = request.getServerName();    // 返回客户端请求的服务器主机名（即 域名 或 IP地址），通常是 HTTP 请求头中的 Host 字段值
        // 获取服务器端口
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = serverName + serverPort + "/" + shortUri;      // 格式 --> 域名:端口/短链接后缀
        // 尝试从Redis缓存中获取完整短链接对象（这个对象中存储值部分的就是原始链接）
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));

        // 如果缓存中存在，重定向到原始链接
        if (StrUtil.isNotBlank(originalLink)) {
            // 先统计访问数据
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, null, statsRecord);
            response.sendRedirect(originalLink);
            return;
        }

        // 不存在，在进行数据库查找时，先判断其布隆过滤器中是否存在（避免缓存穿透，防止大量恶意请求打到数据库）
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        // 不存在
        if (!contains) {
            response.sendRedirect("/page/notfound");    // 重定向到不存在页面
            return;
        }

        // 存在于布隆过滤器中（缓存中不存在），再判断其是否在缓存空数据的对象中
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        // 存在于已经缓存了的空数据对象中，没必要再去查数据库了
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            response.sendRedirect("/page/notfound");    // 重定向到不存在页面
            return;
        }
        // 确实不存在，进入加锁流程

        // 缓存中原始链接对象不存在，加锁进行缓存中数据的重构
        // 获取分布式锁对象
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        // 加锁（阻塞直至获取锁，只会有一个线程加锁成功）
        lock.lock();
        try {
            // 双重判定锁：就获取分布式锁后再次确认数据是否已被其他节点加载，避免重复操作，减少数据库的响应
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            // 存在，重定向到原始链接
            if (StrUtil.isNotBlank(originalLink)) {
                // 统计访问数据
                ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(fullShortUrl, null, statsRecord);

                response.sendRedirect(originalLink);
                return;
            }
            // 缓存中确实不存在当前完整的短链接对象
            // 先根据 fullShortUrl 去 路由表中查找对应的 gid
            LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
            // 路由表中没查找到
            if (shortLinkGotoDO == null) {
                // 当前传递过来的短链接在数据库中根本不存在，缓存空对象到缓存中，设置为 "-" 标识为空对象
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);

                response.sendRedirect("/page/notfound");    // 重定向到不存在页面
                return;
            }

            // 路由表中存在对应的映射关系
            LambdaQueryWrapper<ShortLinkDO> linkDOQueryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)     // 未删除
                    .eq(ShortLinkDO::getEnableStatus, 0);// 启用
            ShortLinkDO shortLinkDO = baseMapper.selectOne(linkDOQueryWrapper);
            // 可以根据当前的完整短链接 和 gid 查找到 对应数据，获取原始链接进行跳转

            // 数据库中不存在 || 数据库中存在，短链接不为空（有效期不为空，避免空指针）且已过期
            if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
                // 这个 before() 方法：判断当前对象是否在参数 when 表示的日期之前
                // 短链接过了有效期，相当于把它当作没有这个短链接一样处理
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl),
                        "-", 30, TimeUnit.MINUTES);

                response.sendRedirect("/page/notfound");    // 重定向到不存在页面
                return;
            }

            // 永久有效 和 有效时间未过期 的短链接
            // 将当前的原始短链接写入缓存，这样下次对于同一个短链接，就可以直接从 Redis 中拿就好了
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidDate(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS      // 设置短链接在缓存中的过期时间
            );

            // 跳转之前先进行短链接统计
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, shortLinkDO.getGid(), statsRecord);
            // 重定向到原始链接
            response.sendRedirect(shortLinkDO.getOriginUrl());

        } finally {
            lock.unlock();      // 释放锁
        }
    }

    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, HttpServletRequest request, HttpServletResponse response) {
        // 创建一个原子布尔值，用于判断是否为第一次访问的uv
        // AtomicBoolean 是一个线程安全的布尔值包装类，支持原子操作，可以在多线程环境下安全地读取和修改布尔值，而无需使用显式的同步机制（如 synchronized）
        // 线程安全：在多线程处理请求时保证数据一致性
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        // 先判断短链接中是否携带cookie
        Cookie[] cookies = request.getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            response.addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        String remoteAddr = LinkUtil.getActualIp(request);
        String os = LinkUtil.getOs(request);
        String browser = LinkUtil.getBrowser(request);
        String device = LinkUtil.getDevice(request);
        String network = LinkUtil.getNetwork(request);
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .build();
    }

    /**
     * 短链接监控统计
     *
     * @param fullShortUrl 完整短链接
     * @param gid          分组标识
     * @param statsRecord  短链接统计实体参数
     */
    @Override
    public void shortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO statsRecord) {
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("fullShortUrl", fullShortUrl);
        producerMap.put("gid", gid);
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));

        // 发送消息给消费者处理短链接监控数据统计逻辑
        shortLinkStatsSaveProducer.send(producerMap);
    }

    /**
     * 获取网站图标
     *
     * @param url 原始链接
     * @return 网站图标地址
     */
    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);   // 创建URL对象
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();      // 打开链接
        connection.setInstanceFollowRedirects(false);       // 禁止自动处理重定向
        connection.setRequestMethod("GET");     // 设置请求方法为get
        connection.connect();       // 连接

        int responseCode = connection.getResponseCode();    // 获取响应码
        // 如果是重定向响应码
        if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
            // 获取重定向响应码
            String redirectUrl = connection.getHeaderField("Location");
            // 如果重定向响应码不为空
            if (redirectUrl != null) {
                URL newUrl = new URL(redirectUrl);      // 创建新的URL对象
                connection = (HttpURLConnection) newUrl.openConnection();       // 打开新的连接
                connection.setRequestMethod("GET");     // 设置请求方法为get
                connection.connect();   // 连接
                responseCode = connection.getResponseCode();    // 获取新的响应码
            }
        }

        // 如果响应码是200（HTTP_OK）
        if (responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();   // 使用Jsoup库连接到URL并获取文档对象
            //选择第一个匹配的<link>标签，其rel属性包含"shortcut"或"icon"
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            // 如果存在favicon图标链接
            if (faviconLink != null) {
                // 返回图标的绝对路径
                return faviconLink.attr("abs:href");
            }
        }
        // 如果不存在favicon图标链接，则返回null
        return null;
    }

    /**
     * 验证跳转原始链接白名单
     *
     * @param originUrl 原始链接
     */
    private void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (enable == null || !enable) {
            return;
        }
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }
}
